import sys
import datetime
import time
import json
import threading
from absl import flags

import pytz
import tornado.ioloop
import tornado.web
import tornado.gen
import tornado.template
from tornado.concurrent import run_on_executor
from tornado_xmlrpc import handler

from coin.tool.basis_server.basis_util import PriceSymbol
from coin.tool.basis_server.basis_util import BasisStorage


# TODO(neo): Store it in memcached so that it can restart
class DataStorage(object):
  _instance = None
  _lock = threading.Lock()

  def __init__(self):
    self._basis_storage = BasisStorage()
    self._cached_basis = {}
    assert DataStorage._instance is None
    DataStorage._instance = self

  def has_basis_group(self, group):
    return group in self._cached_basis

  def get_basis_groups(self):
    return self._cached_basis.keys()

  def get_cached_basis(self, group):
    return self._cached_basis[group]

  @staticmethod
  def get_instance():
    with DataStorage._lock:
      if DataStorage._instance is None:
        DataStorage()
      return DataStorage._instance

  @run_on_executor
  def add_product(self, key):
    self._basis_storage.add_product(key)
    return True

  @run_on_executor
  def add_pair(self, tag, key_a, key_b):
    self._basis_storage.add_pair(tag, key_a, key_b)
    return True

  @run_on_executor
  def update_price(self, key, value, fetched_time):
    self._basis_storage.update_value(key, value, fetched_time)
    return True

  @run_on_executor
  def _update_basis(self, key_a, key_b):
    self._basis_storage.update_basis(key_a, key_b)
    return True

  @run_on_executor
  def _cache_basis(self):
    self._cached_basis = self._basis_storage.get_basis_per_group()

  @tornado.gen.coroutine
  def update_all_basis_periodically(self):
    try:
      futures = []
      for key_a, key_b in self._basis_storage.get_pairs():
        futures.append(self._update_basis(key_a, key_b))
      for future in futures:
        yield future
      yield self._cache_basis()
    finally:
      tornado.ioloop.IOLoop.instance().call_later(0.1, callback=self.update_all_basis_periodically)


class PriceUpdateRpcHandler(handler.XMLRPCHandler):
  """
  exchange: UPBIT, BITHUMB, COINONE, KORBIT
  price_type: last_trade, mid_price_1m
    - 'mid_price_1m' is
        0.5 * ((ask_price after removing 1M KRW liquidity) + 
               (bid_price after removing 1M KRW liquidity))
    - 'last_trade' is the recent trade price.
  ticker: BTC-KRW, BCH-KRW, ETH-KRW, ETC-KRW, QTUM-KRW ...

  The validity of value are caller's responsibility.
  """
  def rpc_update_price(self, group, exchange, ticker, price_type, value):
    storage = DataStorage.get_instance()
    key = PriceSymbol(group, exchange, ticker, price_type)
    fetched_time = time.time()
    result = yield storage.update_price(key, value, fetched_time)
    return result

  def rpc_add_product(self, group, exchange, ticker, price_type):
    storage = DataStorage.get_instance()
    key = PriceSymbol(group, exchange, ticker, price_type)
    result = yield storage.add_product(key)
    return result

  def rpc_add_pair(self,
                   tag,
                   group_a,
                   exchange_a,
                   ticker_a,
                   price_type_a,
                   group_b,
                   exchange_b,
                   ticker_b,
                   price_type_b):
    storage = DataStorage.get_instance()
    key_a = PriceSymbol(group_a, exchange_a, ticker_a, price_type_a)
    key_b = PriceSymbol(group_b, exchange_b, ticker_b, price_type_b)
    result = yield storage.add_pair(tag, key_a, key_b)
    return result


class ApiNaviHandler(tornado.web.RequestHandler):
  def get(self):
    storage = DataStorage.get_instance()
    self.write(json.dumps(list(storage.get_basis_groups()), indent=2, sort_keys=True))


class ApiHandler(tornado.web.RequestHandler):
  def get(self, group):
    storage = DataStorage.get_instance()
    if not storage.has_basis_group(group):
      self.write("")
      return
    self.write(json.dumps(storage.get_cached_basis(group), indent=2, sort_keys=True))


class VisualNaviHandler(tornado.web.RequestHandler):
  def get(self):
    storage = DataStorage.get_instance()
    urls = {}
    for group in storage.get_basis_groups():
      urls[group] = "%s/%s" % (self.request.full_url(), group)
    res = """
    <!DOCTYPE html>
    <html>
    <heade
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Basis Server</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">
    </head>
    <body>
      <section class="section">
      <div class="container">
      <h1 class="title">Available Basis Groups</h1>
      {% for key, url in urls.items() %}
        <a href={{ url }} class="button">{{ key }}</a>
      {% end %}
      </div>
      </section>
    </body>
    </html>
    """
    html = tornado.template.Template(res)
    self.write(html.generate(urls=urls))


class VisualHandler(tornado.web.RequestHandler):
  def get(self, group):
    storage = DataStorage.get_instance()
    if not storage.has_basis_group(group):
      self.write("Not found")
      return

    sgt = pytz.timezone("Asia/Singapore")
    obj = storage.get_cached_basis(group)

    column_set = set()
    for key, elem in obj["basis"].items():
      for window_size in elem["basis"].keys():
        window_size_sec = elem["basis"][window_size]["window_size_sec"]
        column_set.add((window_size, window_size_sec))

    column_list = sorted(list(column_set))
    tables = {}
    for key in sorted(obj["basis"].keys()):
      elem = obj["basis"][key]
      tag = elem["tag"]
      exchange_a = elem["symbol_a"]["exchange"]
      currency_a = elem["symbol_a"]["currency"]
      price_type_a = elem["symbol_a"]["price_type"]
      key_a = elem["symbol_a"]["key"]

      exchange_b = elem["symbol_b"]["exchange"]
      currency_b = elem["symbol_b"]["currency"]
      price_type_b = elem["symbol_b"]["price_type"]
      key_b = elem["symbol_b"]["key"]
      try:
        ref_price = (obj["price"][key_a]["price"] + obj["price"][key_b]["price"]) * 0.5
      except Exception:
        ref_price = 1e-9
      table_key = (exchange_a, exchange_b)
      row = [''] * len(column_list)
      last_time = 0
      for index, column_obj in enumerate(column_list):
        column = column_obj[0]
        if column in elem["basis"]:
          row[index] = ('{:5.2f}%'.format(elem["basis"][column]["value"] / ref_price * 100),
                        '{:12,.2f}'.format(elem["basis"][column]["value"]),
                        elem["basis"][column]["ready"])
          fetched_time = elem["basis"][column]["last_fetched_time_sec"]
          if last_time == 0 or last_time > fetched_time:
            last_time = fetched_time
      last_time_dt = datetime.datetime.fromtimestamp(last_time, tz=sgt)
      last_time_str = (last_time_dt.strftime("%Y-%m-%d"), last_time_dt.strftime("%H:%M:%S %z"))

      if not table_key in tables:
        tables[table_key] = {
            "top": ["%.fs" % elem[1] for elem in column_list],
            "cell_heading": [],
            "last_time": [],
            "ref_price": [],
            "body": []
        }
      tables[table_key]["body"].append(row)
      tables[table_key]["cell_heading"].append(tag)
      tables[table_key]["last_time"].append(last_time_str)
      tables[table_key]["ref_price"].append('{:12,.2f}'.format(ref_price))

    res = """
    <!DOCTYPE html>
    <html>
    <heade
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Basis Server</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">
    </head>
    <body>

    {% for table_key, table in tables.items() %}
      <section class="section">
      <div class="container">
      <h1 class="title">{{ table_key[0] }} - {{ table_key[1] }}</h1>
      <table class="table">
      <thead>
      <th></th>
      {% for top in table["top"] %}
        <td class="has-text-right">{{ top }}</td>
      {% end %}
      </thead>
      <tbody>
      {% for heading, ref_price, last_time, row in zip(table["cell_heading"], table["ref_price"], table["last_time"], table["body"]) %}
        <tr>
        <th>
          <p class="is-size-6">{{ heading }}</p>
          <p class="is-size-7">{{ ref_price }}</p>
        </th>
        {% for value in row %}
          <td class="has-text-right is-light">
            {% if value[2] %}
              <p class="is-size-6">{{ value[0] }}</p>
              <p class="is-size-7">{{ value[1] }}</p>
            {% else %}
              <a class="button is-light is-loading" disabled></a>
            {% end %}
          </td>
        {% end %}
        <td>
          <p class="is-size-7">{{ last_time[0] }}</p>
          <p class="is-size-7">{{ last_time[1] }}</p>
        </td>
        </tr>
      {% end %}
      </tbody>
      </table>
      </div>
      </section>
    {% end %}

    </body>
    </html>
    """
    html = tornado.template.Template(res)
    self.write(html.generate(tables=tables))


def make_viwer():
  app = tornado.web.Application([
      (r"/api", ApiNaviHandler),
      (r"/api/(.*)", ApiHandler),
      (r"/visual", VisualNaviHandler),
      (r"/visual/(.*)", VisualHandler),
  ])
  app.listen(8080)


def make_rpc_server():
  app = tornado.web.Application([
      (r"/RPC2", PriceUpdateRpcHandler),
  ])
  app.listen(8090)


def main(argv):
  try:
    argv = flags.FLAGS(argv)  # parse flags
  except flags.FlagsError as e:
    print('%s\\nUsage: %s ARGS\\n%s' % (e, sys.argv[0], flags.FLAGS))
    sys.exit(1)

  make_viwer()
  make_rpc_server()
  data_storage = DataStorage.get_instance()
  tornado.ioloop.IOLoop.instance().add_callback(callback=data_storage.update_all_basis_periodically)
  tornado.ioloop.IOLoop.instance().start()


if __name__ == "__main__":
  main(sys.argv)
